// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0

package api_test

import (
	"math/rand"
	"reflect"
	"sort"
	"testing"
	"time"

	"github.com/projectcapsule/capsule/pkg/api"
)

func linearFindUser(list api.UserListSpec, name string, kind api.OwnerKind) (api.UserSpec, bool) {
	for _, u := range list {
		if u.Kind == kind && u.Name == name {
			return u, true
		}
	}
	return api.UserSpec{}, false
}

func slowIsPresent(u api.UserListSpec, name string, groups []string) bool {
	for _, user := range u {
		switch user.Kind {
		case api.UserOwner, api.ServiceAccountOwner:
			if name == user.Name {
				return true
			}
		case api.GroupOwner:
			for _, group := range groups {
				if group == user.Name {
					return true
				}
			}
		}
	}
	return false
}

func TestByKindNameOrdering_UserListSpec(t *testing.T) {
	u := api.UserListSpec{
		api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner},
		api.UserSpec{Name: "z", Kind: api.UserOwner},
		api.UserSpec{Name: "a", Kind: api.GroupOwner},
		api.UserSpec{Name: "a", Kind: api.UserOwner},
	}

	// Sort using production ordering
	got := append(api.UserListSpec(nil), u...)
	sort.Sort(api.ByKindName(got))

	// Manually sorted expectation using the same logic.
	want := append(api.UserListSpec(nil), u...)
	sort.Slice(want, func(i, j int) bool {
		if want[i].Kind.String() != want[j].Kind.String() {
			return want[i].Kind.String() < want[j].Kind.String()
		}
		return want[i].Name < want[j].Name
	})

	if len(got) != len(want) {
		t.Fatalf("length mismatch: got %d, want %d", len(got), len(want))
	}
	for i := range got {
		if !reflect.DeepEqual(got[i], want[i]) {
			t.Fatalf("ordering mismatch at %d: got %+v, want %+v", i, got[i], want[i])
		}
	}
}

func TestFindUser_Randomized(t *testing.T) {
	rnd := rand.New(rand.NewSource(42))

	ownerKinds := []api.OwnerKind{
		api.GroupOwner,
		api.UserOwner,
		api.ServiceAccountOwner,
	}

	const (
		numLists          = 200
		maxLength         = 40
		numLookupsPerList = 80
	)

	for listIdx := 0; listIdx < numLists; listIdx++ {
		var list api.UserListSpec
		n := rnd.Intn(maxLength)
		for i := 0; i < n; i++ {
			k := ownerKinds[rnd.Intn(len(ownerKinds))]
			list = append(list, api.UserSpec{
				Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6
				Kind: k,
			})
		}

		for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
			var qName string
			var qKind api.OwnerKind

			if len(list) > 0 && rnd.Float64() < 0.6 {
				// 60% of lookups: pick a real element, must be found
				pick := list[rnd.Intn(len(list))]
				qName = pick.Name
				qKind = pick.Kind
			} else {
				// 40%: random query, may or may not exist
				qName = randomName(rnd, 3+rnd.Intn(4))
				qKind = ownerKinds[rnd.Intn(len(ownerKinds))]
			}

			listCopy := append(api.UserListSpec(nil), list...) // FindUser sorts in-place
			gotUser, gotFound := listCopy.FindUser(qName, qKind)
			wantUser, wantFound := linearFindUser(list, qName, qKind)

			if gotFound != wantFound {
				t.Fatalf("list=%d lookup=%d: found mismatch for (%q,%v): got=%v, want=%v",
					listIdx, lookupIdx, qName, qKind, gotFound, wantFound)
			}
			if gotFound && !reflect.DeepEqual(gotUser, wantUser) {
				t.Fatalf("list=%d lookup=%d: user mismatch for (%q,%v):\n got= %+v\nwant= %+v",
					listIdx, lookupIdx, qName, qKind, gotUser, wantUser)
			}
		}
	}
}

func TestIsPresent_RandomizedMatchesSlowImplementation(t *testing.T) {
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))

	ownerKinds := []api.OwnerKind{
		api.UserOwner,
		api.GroupOwner,
		api.ServiceAccountOwner,
	}

	const (
		numLists          = 200
		maxOwnersPerList  = 30
		numLookupsPerList = 80
		maxGroupsPerUser  = 10
	)

	for listIdx := 0; listIdx < numLists; listIdx++ {
		// Generate a random user list (possibly with duplicates).
		var users api.UserListSpec
		nOwners := rnd.Intn(maxOwnersPerList)
		for i := 0; i < nOwners; i++ {
			kind := ownerKinds[rnd.Intn(len(ownerKinds))]
			users = append(users, api.UserSpec{
				Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6
				Kind: kind,
			})
		}

		for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ {
			// Generate a random userName and groups,
			// sometimes biased to hit existing owners/groups.
			var userName string
			var groups []string

			// 50% of the time: pick an existing owner name as userName
			if len(users) > 0 && rnd.Float64() < 0.5 {
				pick := users[rnd.Intn(len(users))]
				userName = pick.Name
			} else {
				userName = randomName(rnd, 3+rnd.Intn(4))
			}

			// Random groups, sometimes including owner names
			nGroups := rnd.Intn(maxGroupsPerUser)
			for i := 0; i < nGroups; i++ {
				if len(users) > 0 && rnd.Float64() < 0.5 {
					pick := users[rnd.Intn(len(users))]
					groups = append(groups, pick.Name)
				} else {
					groups = append(groups, randomName(rnd, 3+rnd.Intn(4)))
				}
			}

			got := users.IsPresent(userName, groups)
			want := slowIsPresent(users, userName, groups)

			if got != want {
				t.Fatalf("list=%d lookup=%d: mismatch\n  users=%v\n  user=%q\n  groups=%v\n  optimized=%v\n  slow=%v",
					listIdx, lookupIdx, users, userName, groups, got, want)
			}
		}
	}
}

func TestGetByKinds_Basic(t *testing.T) {
	users := api.UserListSpec{
		api.UserSpec{Name: "alice", Kind: api.UserOwner},
		api.UserSpec{Name: "svc-1", Kind: api.ServiceAccountOwner},
		api.UserSpec{Name: "team-a", Kind: api.GroupOwner},
		api.UserSpec{Name: "bob", Kind: api.UserOwner},
		api.UserSpec{Name: "team-b", Kind: api.GroupOwner},
	}

	eqStrings := func(got, want []string) bool {
		if got == nil && want == nil {
			return true
		}
		if len(got) != len(want) {
			return false
		}
		sort.Strings(got)
		sort.Strings(want)
		for i := range got {
			if got[i] != want[i] {
				return false
			}
		}
		return true
	}

	// Single kind: UserOwner
	gotUsers := users.GetByKinds([]api.OwnerKind{api.UserOwner})
	wantUsers := []string{"alice", "bob"}
	if !eqStrings(gotUsers, wantUsers) {
		t.Fatalf("GetByKinds([UserOwner]) = %v, want %v", gotUsers, wantUsers)
	}

	// Single kind: GroupOwner
	gotGroups := users.GetByKinds([]api.OwnerKind{api.GroupOwner})
	wantGroups := []string{"team-a", "team-b"}
	if !eqStrings(gotGroups, wantGroups) {
		t.Fatalf("GetByKinds([GroupOwner]) = %v, want %v", gotGroups, wantGroups)
	}

	// Multiple kinds: UserOwner + ServiceAccountOwner
	gotUsersAndSAs := users.GetByKinds([]api.OwnerKind{api.UserOwner, api.ServiceAccountOwner})
	wantUsersAndSAs := []string{"alice", "bob", "svc-1"}
	if !eqStrings(gotUsersAndSAs, wantUsersAndSAs) {
		t.Fatalf("GetByKinds([UserOwner,ServiceAccountOwner]) = %v, want %v",
			gotUsersAndSAs, wantUsersAndSAs)
	}

	// No kinds → nil
	gotNone := users.GetByKinds(nil)
	if gotNone != nil {
		t.Fatalf("GetByKinds(nil) = %v, want nil", gotNone)
	}

	// Kind not present at all
	gotUnknown := users.GetByKinds([]api.OwnerKind{api.OwnerKind("does-not-exist")})
	if gotUnknown != nil {
		t.Fatalf("GetByKinds([unknown]) = %v, want nil", gotUnknown)
	}
}

func TestGetByKinds_Randomized(t *testing.T) {
	rnd := rand.New(rand.NewSource(123))

	ownerKinds := []api.OwnerKind{
		api.UserOwner,
		api.GroupOwner,
		api.ServiceAccountOwner,
	}

	const (
		numLists         = 200
		maxOwnersPerList = 50
	)

	for listIdx := 0; listIdx < numLists; listIdx++ {
		var users api.UserListSpec
		n := rnd.Intn(maxOwnersPerList)
		for i := 0; i < n; i++ {
			k := ownerKinds[rnd.Intn(len(ownerKinds))]
			users = append(users, api.UserSpec{
				Name: randomName(rnd, 3+rnd.Intn(4)), // reuse your helper
				Kind: k,
			})
		}

		// Try several random kind-subsets per list
		for subsetIdx := 0; subsetIdx < 10; subsetIdx++ {
			// Build a random subset of kinds
			var kinds []api.OwnerKind
			for _, k := range ownerKinds {
				if rnd.Float64() < 0.5 {
					kinds = append(kinds, k)
				}
			}

			got := users.GetByKinds(kinds)

			// Reference implementation: filter + sort
			kindSet := make(map[api.OwnerKind]struct{}, len(kinds))
			for _, k := range kinds {
				kindSet[k] = struct{}{}
			}

			var want []string
			if len(kinds) > 0 {
				for _, u := range users {
					if _, ok := kindSet[u.Kind]; ok {
						want = append(want, u.Name)
					}
				}
			}

			if len(want) == 0 {
				if got != nil {
					t.Fatalf("list=%d subset=%d: expected nil, got %v (kinds=%v, users=%v)",
						listIdx, subsetIdx, got, kinds, users)
				}
				continue
			}

			sort.Strings(want)
			sort.Strings(got)

			if !reflect.DeepEqual(got, want) {
				t.Fatalf("list=%d subset=%d: GetByKinds mismatch\n  kinds=%v\n  got=  %v\n  want= %v",
					listIdx, subsetIdx, kinds, got, want)
			}
		}
	}
}
